/**
 * Copyright 2012 Kamran Zafar 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 * 
 */

package org.xeustechnologies.jtar;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * @author Kamran Zafar
 * 
 */
public class TarInputStream extends FilterInputStream {

    private static final int SKIP_BUFFER_SIZE = 2048;
    private TarEntry currentEntry;
    private long currentFileSize;
    private long bytesRead;
    private boolean defaultSkip = false;

    public TarInputStream(InputStream in) {
        super( in );
        currentFileSize = 0;
        bytesRead = 0;
    }

    @Override
    public boolean markSupported() {
        return false;
    }

    /**
     * Not supported
     * 
     */
    @Override
    public synchronized void mark(int readlimit) {
    }

    /**
     * Not supported
     * 
     */
    @Override
    public synchronized void reset() throws IOException {
        throw new IOException( "mark/reset not supported" );
    }

    /**
     * Read a byte
     * 
     * @see FilterInputStream#read()
     */
    @Override
    public int read() throws IOException {
        byte[] buf = new byte[1];

        int res = this.read( buf, 0, 1 );

        if (res != -1) {
            return buf[0];
        }

        return res;
    }

    /**
     * Checks if the bytes being read exceed the entry size and adjusts the byte
     * array length. Updates the byte counters
     *
     *
     * @see FilterInputStream#read(byte[], int, int)
     */
    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        if (currentEntry != null) {
            if (currentFileSize == currentEntry.getSize()) {
                return -1;
            } else if (( currentEntry.getSize() - currentFileSize ) < len) {
                len = (int) ( currentEntry.getSize() - currentFileSize );
            }
        }

        int br = super.read( b, off, len );

        if (br != -1) {
            if (currentEntry != null) {
                currentFileSize += br;
            }

            bytesRead += br;
        }

        return br;
    }

    /**
     * Returns the next entry in the tar file
     * 
     * @return TarEntry
     * @throws IOException
     */
    public TarEntry getNextEntry() throws IOException {
        closeCurrentEntry();

        byte[] header = new byte[TarConstants.HEADER_BLOCK];
        byte[] theader = new byte[TarConstants.HEADER_BLOCK];
        int tr = 0;

        // Read full header
        while (tr < TarConstants.HEADER_BLOCK) {
            int res = read( theader, 0, TarConstants.HEADER_BLOCK - tr );

            if (res < 0) {
                break;
            }

            System.arraycopy( theader, 0, header, tr, res );
            tr += res;
        }

        // Check if record is null
        boolean eof = true;
        for (byte b : header) {
            if (b != 0) {
                eof = false;
                break;
            }
        }

        if (!eof) {
            bytesRead += header.length;
            currentEntry = new TarEntry( header );
        }

        return currentEntry;
    }

    /**
     * Closes the current tar entry
     * 
     * @throws IOException
     */
    protected void closeCurrentEntry() throws IOException {
        if (currentEntry != null) {
            if (currentEntry.getSize() > currentFileSize) {
                // Not fully read, skip rest of the bytes
                long bs = 0;
                while (bs < currentEntry.getSize() - currentFileSize) {
                    long res = skip( currentEntry.getSize() - currentFileSize - bs );

                    if (res == 0 && currentEntry.getSize() - currentFileSize > 0) {
                        throw new IOException( "Possible tar file corruption" );
                    }

                    bs += res;
                }
            }

            currentEntry = null;
            currentFileSize = 0L;
            skipPad();
        }
    }

    /**
     * Skips the pad at the end of each tar entry file content
     * 
     * @throws IOException
     */
    protected void skipPad() throws IOException {
        if (bytesRead > 0) {
            int extra = (int) ( bytesRead % TarConstants.DATA_BLOCK );

            if (extra > 0) {
                long bs = 0;
                while (bs < TarConstants.DATA_BLOCK - extra) {
                    long res = skip( TarConstants.DATA_BLOCK - extra - bs );
                    bs += res;
                }
            }
        }
    }

    /**
     * Skips 'n' bytes on the InputStream<br>
     * Overrides default implementation of skip
     * 
     */
    @Override
    public long skip(long n) throws IOException {
        if (defaultSkip) {
            // use skip method of parent stream
            // may not work if skip not implemented by parent
            return super.skip( n );
        }

        if (n <= 0) {
            return 0;
        }

        long left = n;
        byte[] sBuff = new byte[SKIP_BUFFER_SIZE];

        while (left > 0) {
            int res = read( sBuff, 0, (int) ( left < SKIP_BUFFER_SIZE ? left : SKIP_BUFFER_SIZE ) );
            if (res < 0) {
                break;
            }
            left -= res;
        }

        return n - left;
    }

    public boolean isDefaultSkip() {
        return defaultSkip;
    }

    public void setDefaultSkip(boolean defaultSkip) {
        this.defaultSkip = defaultSkip;
    }
}
